Repository navigation
i18n: localize the 9.0.1 What's New title (unblock the coverage gate) - #514
Merged
Merged
Conversation
The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (#507, #512, #513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
This was referenced Jul 16, 2026
ryanbr
added a commit
that referenced
this pull request
Jul 17, 2026
A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the What's New title as a RAW Kotlin literal that has to be localized by hand afterwards. With a PR-only trigger that landed completely unaudited — and because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed by #514): the break was invisible where it was introduced and highly visible everywhere it wasn't. Audit pushes to main so a release-introduced literal surfaces on main the moment it lands, and gets fixed before any contributor trips over it. Same 8-second ubuntu job, no new cost. github.base_ref is set only for pull_request and is empty on a push (which would pass a bare 'origin/'), so fall back to main. The ref is retained for workflow compatibility — --ci is a whole-tree gate, so the resolved value does not change the verdict. YAML parses (both triggers); the push path's exact command 'i18n_audit.py --ci origin/main' passes on current main.
ryanbr
added a commit
that referenced
this pull request
Jul 17, 2026
Numbers are main's current 292/203 +1, matching the previous staging commit's pattern, so the .staging app upgrades cleanly over the last testing build (290) and stays above main's released high-water. Carries everything merged since the 9.0.1 release: - Workouts read HR under the recording strap, not a hardcoded id (#512 Kotlin, #513 Swift twin) - Oura 0x60/0x80 IBI decoders corrected to the ring's real byte layout (#511) - Whoop5Config enable_sig12 corrected to ASCII '1' (#522) - Sleep time edits save as one validated window (#525) - "Recompute this night" for deleted sleep windows (#526) - Stress marker values stay readable (#507) - Faster history sync + faster Bluetooth link, both EXPERIMENTAL and default-off (#536/#537/#538) - the whole point of this build is field data on those - i18n changelog-title fix + main-push audit (#514/#535) versionName stays 9.0.2 here for the staging identity only; the real 9.0.2 release still bumps main separately (and must be raised past 293 first).
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 17, 2026
…variant ci_check() audited the whole merged tree unconditionally once the ryanbr#453 backlog closed, ignoring base_ref entirely. That means a transient regression on main (the 9.0.1 release generating a raw AppChangelog.kt literal, fixed by ryanbr#514) red-flagged every open PR whose diff never touched the offending file — and those PRs stayed red, since a pull_request-triggered workflow doesn't re-run just because main changed. ryanbr#508 and ryanbr#506 are still showing that stale failure right now, over 5 hours after ryanbr#514 fixed main. Restores the base_ref diff (Android side already had the git_show/ literals_at_ref scaffolding for this, just unused) across every check: hardcoded literals, missing-translation keys, and format-string mismatches, on both platforms. A violation already present on base_ref isn't this PR's fault and no longer fails it; existing debt is still printed for visibility. Today that's zero everywhere, so the practical behavior on a healthy main is unchanged — this only changes what happens when main itself regresses.
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 17, 2026
…variant ci_check() audited the whole merged tree unconditionally once the ryanbr#453 backlog closed, ignoring base_ref entirely. That means a transient regression on main (the 9.0.1 release generating a raw AppChangelog.kt literal, fixed by ryanbr#514) red-flagged every open PR whose diff never touched the offending file — and those PRs stayed red, since a pull_request-triggered workflow doesn't re-run just because main changed. ryanbr#508 and ryanbr#506 are still showing that stale failure right now, over 5 hours after ryanbr#514 fixed main. Restores the base_ref diff (Android side already had the git_show/ literals_at_ref scaffolding for this, just unused) across every check: hardcoded literals, missing-translation keys, and format-string mismatches, on both platforms. A violation already present on base_ref isn't this PR's fault and no longer fails it; existing debt is still printed for visibility. Today that's zero everywhere, so the practical behavior on a healthy main is unchanged — this only changes what happens when main itself regresses.
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 19, 2026
…variant ci_check() audited the whole merged tree unconditionally once the ryanbr#453 backlog closed, ignoring base_ref entirely. That means a transient regression on main (the 9.0.1 release generating a raw AppChangelog.kt literal, fixed by ryanbr#514) red-flagged every open PR whose diff never touched the offending file — and those PRs stayed red, since a pull_request-triggered workflow doesn't re-run just because main changed. ryanbr#508 and ryanbr#506 are still showing that stale failure right now, over 5 hours after ryanbr#514 fixed main. Restores the base_ref diff (Android side already had the git_show/ literals_at_ref scaffolding for this, just unused) across every check: hardcoded literals, missing-translation keys, and format-string mismatches, on both platforms. A violation already present on base_ref isn't this PR's fault and no longer fails it; existing debt is still printed for visibility. Today that's zero everywhere, so the practical behavior on a healthy main is unchanged — this only changes what happens when main itself regresses.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ryanbr#514) The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (ryanbr#507, ryanbr#512, ryanbr#513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the What's New title as a RAW Kotlin literal that has to be localized by hand afterwards. With a PR-only trigger that landed completely unaudited — and because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed by ryanbr#514): the break was invisible where it was introduced and highly visible everywhere it wasn't. Audit pushes to main so a release-introduced literal surfaces on main the moment it lands, and gets fixed before any contributor trips over it. Same 8-second ubuntu job, no new cost. github.base_ref is set only for pull_request and is empty on a push (which would pass a bare 'origin/'), so fall back to main. The ref is retained for workflow compatibility — --ci is a whole-tree gate, so the resolved value does not change the verdict. YAML parses (both triggers); the push path's exact command 'i18n_audit.py --ci origin/main' passes on current main.
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 25, 2026
…variant ci_check() audited the whole merged tree unconditionally once the ryanbr#453 backlog closed, ignoring base_ref entirely. That means a transient regression on main (the 9.0.1 release generating a raw AppChangelog.kt literal, fixed by ryanbr#514) red-flagged every open PR whose diff never touched the offending file — and those PRs stayed red, since a pull_request-triggered workflow doesn't re-run just because main changed. ryanbr#508 and ryanbr#506 are still showing that stale failure right now, over 5 hours after ryanbr#514 fixed main. Restores the base_ref diff (Android side already had the git_show/ literals_at_ref scaffolding for this, just unused) across every check: hardcoded literals, missing-translation keys, and format-string mismatches, on both platforms. A violation already present on base_ref isn't this PR's fault and no longer fails it; existing debt is still printed for visibility. Today that's zero everywhere, so the practical behavior on a healthy main is unchanged — this only changes what happens when main itself regresses.
ryanbr
added a commit
that referenced
this pull request
Jul 27, 2026
Kotlin and Swift attach a doc block to the declaration that FOLLOWS it, and keep only the LAST of several stacked ones. Insert a block above an existing one, or a declaration into the gap between a comment and the thing it documents, and the text is silently orphaned — still there, still above the right function, still correct prose, but no longer reaching Dokka or IDE hover. That is why review does not catch it. #846 fixed three sites in one day, one of which #842 had introduced that same morning, and the block that came adrift was "Byte-parity twin of Swift classifyCoverage" — a parity-contract statement, and parity is the first rule in CLAUDE.md. That is the case for gating rather than only fixing. Tools/doc_comment_lint.py catches both detaching patterns across Kotlin and Swift: stacked blocks, and a blank line between a doc block and its declaration. File headers (a blank line before package/import/@file:) are exempt, since that spacing is conventional and correct. Corrects #846, which claimed the tree was clean afterwards. It was not: that sweep only matched SINGLE-LINE blocks. Handling multi-line blocks finds 25 more pre-existing sites across 13 files — 8 in WhoopBleClient.kt alone, and AppViewModel.kt:68 is a class doc detached by an inserted data class, the same shape as the #842 slip. Those 25 are grandfathered per-file so the gate starts green and blocks only NEW ones. No existing code can red-check a contributor on a line they did not touch — the #514 failure mode. Baselined by COUNT, not line number: line numbers churn on every edit above them, which keeps the file stale and trains people to regenerate it without reading it. A count moves only when someone adds or removes a site. It ratchets DOWN — fixing one prints an IMPROVED line asking for the allowance to be lowered. Verified five ways: passes on clean main; catches all three pre-#846 sites when reconstructed; fails on a new site in an unbaselined file; fails on an EXTRA site in an already-baselined file (so the count is enforced, not mere presence); and clears nine false-positive probes including file headers, a doc above an annotation, and a `*/` appearing inside prose. Pure Python on ubuntu, six seconds. No Xcode or Gradle, so unlike the app-target and Android checks it actually runs while app-build.yml and android.yml are disabled. Its own job gates itself. New workflow rather than a step in i18n-coverage.yml: different concern, and that job is currently the only thing gating PRs — a docs regression should not report itself as an i18n failure. Not included: fixing the 25. Each needs a judgement call about how to merge two blocks, and doing 25 of those at volume is how the mistake gets made. The baseline makes them visible and burnable-down without blocking anyone. Note the 25 are machine-detected; three were spot-checked as genuine, the rest are grandfathered either way.
ryanbr
added a commit
that referenced
this pull request
Jul 27, 2026
Tools/i18n_audit.py:34 was LANGS = ["de","es","fr","pt-PT"]. The Apple catalogs ship EIGHT non-English localizations, so the gate covered four of them — and the four it did not cover drifted exactly as you would expect: catalog de/es/fr/pt-PT it ru zh-Hans zh-Hant Strand (3255 keys) 100% 3086 3110 3136 3086 StrandDesign (95) 100% 14 41 20 14 NOOPWatch (52) 100% 49 50 49 49 NOOPWatchComplications (30) 100% 5 30 5 5 The gated four are at 100% everywhere. The ungated four are missing up to 85% — StrandDesign ships 14 of 95 Italian, the watch complications 5 of 30. That is the gate working where it is pointed and nothing happening where it is not. Android is the same: ANDROID_LOCALE_DIRS hardcoded the same four, values-zh existed on disk, was never compared, and sat 43 keys behind. That is how #834 passed CI while adding a key to four of five locale files (#844). Both lists are now DISCOVERED — Apple languages from the union of each catalog own localizations, Android dirs from values-* on disk — so a locale is covered the day it appears rather than the day someone remembers a constant. LANGS keeps ZERO tolerance and is untouched: those four are perfect precisely because they are hard-gated. The newly covered locales carry 1019 real pre-existing gaps and are gated against a ratcheting allowance instead; switching them on hard would red-check every open PR on lines nobody touched, the exact #514 failure this workflow own comments warn about. Counts rather than key lists, same reasoning as doc_comment_lint_baseline (#849) — a key list goes stale on every edit and trains people to regenerate it unread. Ratchets DOWN, printing IMPROVED when a count drops and STALE for an allowance whose target no longer exists. catalog_summary() had the same blind spot and is fixed too: it reported four of eight languages, so the human-readable coverage view read 0 missing across the board. That is very likely WHY this went unnoticed — anyone checking coverage was shown a clean sheet. It now doubles as the burn-down view for the 1019. Verified: green on main; fires when the values-zh gap grows 43->44; fires when one StrandDesign Italian string is removed; de still hard-fails on a single missing key, so zero tolerance is intact; a fabricated allowance target is reported STALE; and the locale section states its own verdict even when an earlier check failed (it keyed on the global failed flag, so a broken German string silenced it entirely). Translates nothing. It stops the hole growing and makes the debt countable. Whether it/ru/zh-* should ship at 14/95 or be withheld until filled is a product call, not a linter one.
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 27, 2026
…variant ci_check() audited the whole merged tree unconditionally once the ryanbr#453 backlog closed, ignoring base_ref entirely. That means a transient regression on main (the 9.0.1 release generating a raw AppChangelog.kt literal, fixed by ryanbr#514) red-flagged every open PR whose diff never touched the offending file — and those PRs stayed red, since a pull_request-triggered workflow doesn't re-run just because main changed. ryanbr#508 and ryanbr#506 are still showing that stale failure right now, over 5 hours after ryanbr#514 fixed main. Restores the base_ref diff (Android side already had the git_show/ literals_at_ref scaffolding for this, just unused) across every check: hardcoded literals, missing-translation keys, and format-string mismatches, on both platforms. A violation already present on base_ref isn't this PR's fault and no longer fails it; existing debt is still printed for visibility. Today that's zero everywhere, so the practical behavior on a healthy main is unchanged — this only changes what happens when main itself regresses.
digitalerdude
added a commit
to digitalerdude/noop
that referenced
this pull request
Jul 27, 2026
The 9.2.0 release generated its Android AppChangelog title as a raw Kotlin literal instead of uiString(R.string....), the same pattern as ryanbr#514. The i18n audit's regression gate now catches it on every open PR that merges against the current main, including this one, even though this branch never touched that file. Localize it like the other entries: uiString(R.string. l10n_app_changelog_hrv_accuracy_fix_oura_sleep_and_4ed31e39), with the string added to values/ (en) + de/es/fr/pt-rPT (focus locales) and zh (tracked under the ratcheting non-focus-locale allowance). Key follows the established scheme (first-6-word slug + sha1(title)[:8]). i18n_audit.py --ci: OK, no new hardcoded literals, no new locale gaps. compileFullDebugKotlin: OK.
ryanbr
pushed a commit
that referenced
this pull request
Aug 2, 2026
* Distinguish "connected, no data" from "not connected" (#612) The strap-recording chip only checked `connected` in its first branch (`.recording`). A strap that is genuinely connected but has never once synced — a fresh pairing, or a WHOOP-4/generic strap whose recent offloads are a sustained streak of empty — fell through to `.notRecording` and showed "Strap not connected. Tap to connect.", which is simply false: the link is up. That's the exact symptom #612 describes ("the app doesn't say what's really happening") for the case that isn't already covered by #580's `.historyExperimental` override (which still takes priority for a WHOOP 5/MG past its 2-empty-offload threshold — unchanged here). Adds `LiveState.sustainedEmptyOffload`, exposing the WHOOP-4/generic empty-offload streak (`EmptySyncTracker` / Android's twin) that was previously only reachable as free text in `lastSyncError`. Set on every HISTORY_COMPLETE (so a productive sync clears it immediately), reset on disconnect (display flag only — the underlying streak counter deliberately survives a reconnect, unchanged). `RecordingState` gains `.connectedNoData`: connected, no live HR, and either never-synced or sustained-empty. Kotlin gets the identical case, param and priority order in `TodayScoring.kt`, per `RecordingState`'s own doc-comment promise of a 1:1 Kotlin mirror — though note Android's `RecordingStatusChip`/`recordingStateFor` are currently unwired into any screen (pre-existing, unrelated gap; will flag separately). Deliberately not done: no new, more sensitive trigger (the existing 3×/2× consecutive-empty thresholds are unchanged), and no clickable single-bond README hint (belongs a layer above the pure `RecordingState` enum — a smaller follow-up). Verified: xcodebuild build for Strand (macOS) and NOOPiOS. StrandTests/TodayExplainabilityTests (46/46, forcing -testLanguage en -testRegion US — this Mac's system locale is German, which the existing copy tests are sensitive to regardless of this change). `./gradlew compileFullDebugKotlin` clean. Android's `testFullDebugUnitTest` could not be run locally — an unrelated, pre-existing gap (three FakeRegistryDao test fakes are missing ~25 methods DeviceRegistryDao now declares) fails test compilation for the whole module; filed separately, not touched here. Not verified on real hardware this session (no strap connected). Both the regression case (a healthy WHOOP 5.0 session still reading "Recording") and the new `.connectedNoData` case (a fresh pairing, or a WHOOP-4/generic strap's 3rd consecutive empty offload) need a live check before this leaves draft. No Android device+strap test setup either way — that side is compile/unit-test verified only. * Add the missing String Catalog entries for connectedNoData (#612 review) RecordingState.connectedNoData's detail and accessibilityText used String(localized:), the right mechanism, but the entries never got added to Strand/Resources/Localizable.xcstrings — so de/es/fr/pt-PT (and the other languages already carried by every neighbouring chip string) fell back to English. i18n_audit's CI gate couldn't see this: it verifies translation completeness for keys already IN the catalog, not for strings that never made it in at all (the same blind spot as the Text(variable) gap #245's review caught). Verified: Tools/i18n_audit.py --ci origin/main clean, StrandTests TodayExplainabilityTests 46/46 (-testLanguage en -testRegion US). * i18n: localize the 9.2.0 changelog title (unblock the coverage gate) The 9.2.0 release generated its Android AppChangelog title as a raw Kotlin literal instead of uiString(R.string....), the same pattern as #514. The i18n audit's regression gate now catches it on every open PR that merges against the current main, including this one, even though this branch never touched that file. Localize it like the other entries: uiString(R.string. l10n_app_changelog_hrv_accuracy_fix_oura_sleep_and_4ed31e39), with the string added to values/ (en) + de/es/fr/pt-rPT (focus locales) and zh (tracked under the ratcheting non-focus-locale allowance). Key follows the established scheme (first-6-word slug + sha1(title)[:8]). i18n_audit.py --ci: OK, no new hardcoded literals, no new locale gaps. compileFullDebugKotlin: OK.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ryanbr#514) The 9.0.1 release generated its AppChangelog title as a raw Kotlin literal (title = "German, French & Spanish, ..."), unlike every other entry which uses uiString(R.string.l10n_app_changelog_...). The full-tree i18n audit flags the raw literal, and since i18n-coverage runs only on pull_request (never on the release's push to main), it landed unaudited and now red-checks every open PR (ryanbr#507, ryanbr#512, ryanbr#513) on a line none of them touched. Localize it like the prior entries: title -> uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2), with the string added to values/ (en) + the de/es/fr focus locales. Key follows the established scheme (first-6-word slug + sha1(title)[:8]; verified it reproduces the 9.0.0/8.7.0/8.6.2 keys). i18n_audit.py --ci: OK no hardcoded literals. compileFullDebugKotlin: OK. Apple side is unaffected (SwiftUI auto-extracts literals).
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the What's New title as a RAW Kotlin literal that has to be localized by hand afterwards. With a PR-only trigger that landed completely unaudited — and because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed by ryanbr#514): the break was invisible where it was introduced and highly visible everywhere it wasn't. Audit pushes to main so a release-introduced literal surfaces on main the moment it lands, and gets fixed before any contributor trips over it. Same 8-second ubuntu job, no new cost. github.base_ref is set only for pull_request and is empty on a push (which would pass a bare 'origin/'), so fall back to main. The ref is retained for workflow compatibility — --ci is a whole-tree gate, so the resolved value does not change the verdict. YAML parses (both triggers); the push path's exact command 'i18n_audit.py --ci origin/main' passes on current main.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
Kotlin and Swift attach a doc block to the declaration that FOLLOWS it, and keep only the LAST of several stacked ones. Insert a block above an existing one, or a declaration into the gap between a comment and the thing it documents, and the text is silently orphaned — still there, still above the right function, still correct prose, but no longer reaching Dokka or IDE hover. That is why review does not catch it. ryanbr#846 fixed three sites in one day, one of which ryanbr#842 had introduced that same morning, and the block that came adrift was "Byte-parity twin of Swift classifyCoverage" — a parity-contract statement, and parity is the first rule in CLAUDE.md. That is the case for gating rather than only fixing. Tools/doc_comment_lint.py catches both detaching patterns across Kotlin and Swift: stacked blocks, and a blank line between a doc block and its declaration. File headers (a blank line before package/import/@file:) are exempt, since that spacing is conventional and correct. Corrects ryanbr#846, which claimed the tree was clean afterwards. It was not: that sweep only matched SINGLE-LINE blocks. Handling multi-line blocks finds 25 more pre-existing sites across 13 files — 8 in WhoopBleClient.kt alone, and AppViewModel.kt:68 is a class doc detached by an inserted data class, the same shape as the ryanbr#842 slip. Those 25 are grandfathered per-file so the gate starts green and blocks only NEW ones. No existing code can red-check a contributor on a line they did not touch — the ryanbr#514 failure mode. Baselined by COUNT, not line number: line numbers churn on every edit above them, which keeps the file stale and trains people to regenerate it without reading it. A count moves only when someone adds or removes a site. It ratchets DOWN — fixing one prints an IMPROVED line asking for the allowance to be lowered. Verified five ways: passes on clean main; catches all three pre-ryanbr#846 sites when reconstructed; fails on a new site in an unbaselined file; fails on an EXTRA site in an already-baselined file (so the count is enforced, not mere presence); and clears nine false-positive probes including file headers, a doc above an annotation, and a `*/` appearing inside prose. Pure Python on ubuntu, six seconds. No Xcode or Gradle, so unlike the app-target and Android checks it actually runs while app-build.yml and android.yml are disabled. Its own job gates itself. New workflow rather than a step in i18n-coverage.yml: different concern, and that job is currently the only thing gating PRs — a docs regression should not report itself as an i18n failure. Not included: fixing the 25. Each needs a judgement call about how to merge two blocks, and doing 25 of those at volume is how the mistake gets made. The baseline makes them visible and burnable-down without blocking anyone. Note the 25 are machine-detected; three were spot-checked as genuine, the rest are grandfathered either way.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…nbr#850) Tools/i18n_audit.py:34 was LANGS = ["de","es","fr","pt-PT"]. The Apple catalogs ship EIGHT non-English localizations, so the gate covered four of them — and the four it did not cover drifted exactly as you would expect: catalog de/es/fr/pt-PT it ru zh-Hans zh-Hant Strand (3255 keys) 100% 3086 3110 3136 3086 StrandDesign (95) 100% 14 41 20 14 NOOPWatch (52) 100% 49 50 49 49 NOOPWatchComplications (30) 100% 5 30 5 5 The gated four are at 100% everywhere. The ungated four are missing up to 85% — StrandDesign ships 14 of 95 Italian, the watch complications 5 of 30. That is the gate working where it is pointed and nothing happening where it is not. Android is the same: ANDROID_LOCALE_DIRS hardcoded the same four, values-zh existed on disk, was never compared, and sat 43 keys behind. That is how ryanbr#834 passed CI while adding a key to four of five locale files (ryanbr#844). Both lists are now DISCOVERED — Apple languages from the union of each catalog own localizations, Android dirs from values-* on disk — so a locale is covered the day it appears rather than the day someone remembers a constant. LANGS keeps ZERO tolerance and is untouched: those four are perfect precisely because they are hard-gated. The newly covered locales carry 1019 real pre-existing gaps and are gated against a ratcheting allowance instead; switching them on hard would red-check every open PR on lines nobody touched, the exact ryanbr#514 failure this workflow own comments warn about. Counts rather than key lists, same reasoning as doc_comment_lint_baseline (ryanbr#849) — a key list goes stale on every edit and trains people to regenerate it unread. Ratchets DOWN, printing IMPROVED when a count drops and STALE for an allowance whose target no longer exists. catalog_summary() had the same blind spot and is fixed too: it reported four of eight languages, so the human-readable coverage view read 0 missing across the board. That is very likely WHY this went unnoticed — anyone checking coverage was shown a clean sheet. It now doubles as the burn-down view for the 1019. Verified: green on main; fires when the values-zh gap grows 43->44; fires when one StrandDesign Italian string is removed; de still hard-fails on a single missing key, so zero tolerance is intact; a fabricated allowance target is reported STALE; and the locale section states its own verdict even when an earlier check failed (it keyed on the global failed flag, so a broken German string silenced it entirely). Translates nothing. It stops the hole growing and makes the debt countable. Whether it/ru/zh-* should ship at 14/95 or be withheld until filled is a product call, not a linter one.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…yanbr#826) * Distinguish "connected, no data" from "not connected" (ryanbr#612) The strap-recording chip only checked `connected` in its first branch (`.recording`). A strap that is genuinely connected but has never once synced — a fresh pairing, or a WHOOP-4/generic strap whose recent offloads are a sustained streak of empty — fell through to `.notRecording` and showed "Strap not connected. Tap to connect.", which is simply false: the link is up. That's the exact symptom ryanbr#612 describes ("the app doesn't say what's really happening") for the case that isn't already covered by ryanbr#580's `.historyExperimental` override (which still takes priority for a WHOOP 5/MG past its 2-empty-offload threshold — unchanged here). Adds `LiveState.sustainedEmptyOffload`, exposing the WHOOP-4/generic empty-offload streak (`EmptySyncTracker` / Android's twin) that was previously only reachable as free text in `lastSyncError`. Set on every HISTORY_COMPLETE (so a productive sync clears it immediately), reset on disconnect (display flag only — the underlying streak counter deliberately survives a reconnect, unchanged). `RecordingState` gains `.connectedNoData`: connected, no live HR, and either never-synced or sustained-empty. Kotlin gets the identical case, param and priority order in `TodayScoring.kt`, per `RecordingState`'s own doc-comment promise of a 1:1 Kotlin mirror — though note Android's `RecordingStatusChip`/`recordingStateFor` are currently unwired into any screen (pre-existing, unrelated gap; will flag separately). Deliberately not done: no new, more sensitive trigger (the existing 3×/2× consecutive-empty thresholds are unchanged), and no clickable single-bond README hint (belongs a layer above the pure `RecordingState` enum — a smaller follow-up). Verified: xcodebuild build for Strand (macOS) and NOOPiOS. StrandTests/TodayExplainabilityTests (46/46, forcing -testLanguage en -testRegion US — this Mac's system locale is German, which the existing copy tests are sensitive to regardless of this change). `./gradlew compileFullDebugKotlin` clean. Android's `testFullDebugUnitTest` could not be run locally — an unrelated, pre-existing gap (three FakeRegistryDao test fakes are missing ~25 methods DeviceRegistryDao now declares) fails test compilation for the whole module; filed separately, not touched here. Not verified on real hardware this session (no strap connected). Both the regression case (a healthy WHOOP 5.0 session still reading "Recording") and the new `.connectedNoData` case (a fresh pairing, or a WHOOP-4/generic strap's 3rd consecutive empty offload) need a live check before this leaves draft. No Android device+strap test setup either way — that side is compile/unit-test verified only. * Add the missing String Catalog entries for connectedNoData (ryanbr#612 review) RecordingState.connectedNoData's detail and accessibilityText used String(localized:), the right mechanism, but the entries never got added to Strand/Resources/Localizable.xcstrings — so de/es/fr/pt-PT (and the other languages already carried by every neighbouring chip string) fell back to English. i18n_audit's CI gate couldn't see this: it verifies translation completeness for keys already IN the catalog, not for strings that never made it in at all (the same blind spot as the Text(variable) gap ryanbr#245's review caught). Verified: Tools/i18n_audit.py --ci origin/main clean, StrandTests TodayExplainabilityTests 46/46 (-testLanguage en -testRegion US). * i18n: localize the 9.2.0 changelog title (unblock the coverage gate) The 9.2.0 release generated its Android AppChangelog title as a raw Kotlin literal instead of uiString(R.string....), the same pattern as ryanbr#514. The i18n audit's regression gate now catches it on every open PR that merges against the current main, including this one, even though this branch never touched that file. Localize it like the other entries: uiString(R.string. l10n_app_changelog_hrv_accuracy_fix_oura_sleep_and_4ed31e39), with the string added to values/ (en) + de/es/fr/pt-rPT (focus locales) and zh (tracked under the ratcheting non-focus-locale allowance). Key follows the established scheme (first-6-word slug + sha1(title)[:8]). i18n_audit.py --ci: OK, no new hardcoded literals, no new locale gaps. compileFullDebugKotlin: OK.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The 9.0.1 release generated its What's New title into
AppChangelog.ktas a raw Kotlin literal:Every other entry uses a localized resource (
uiString(R.string.l10n_app_changelog_…)). The full-tree i18n audit (Tools/i18n_audit.py) flags the rawtitle = "…"— and because i18n-coverage runs only onpull_request, never on the release's push tomain, it landed unaudited and now red-checks every open PR (#507, #512, #513) on a line none of them touched.Fix
Localize the title exactly like the prior entries:
AppChangelog.kt→title = uiString(R.string.l10n_app_changelog_german_french_spanish_pull_to_sync_1109bda2)values/(en) and thede/es/frfocus locales (the coverage half of the gate needs all three).The key follows the established scheme — first-6-word slug +
sha1(title)[:8]— verified to reproduce the existing 9.0.0 / 8.7.0 / 8.6.2 keys.Verification
python3 Tools/i18n_audit.py --ci origin/main→ OK no hardcoded literals (exit 0)./gradlew compileFullDebugKotlin→ BUILD SUCCESSFUL (R.string resolves, XML well-formed)Scope / follow-ups (not in this PR)
appchangelog-gen.pyemit the localized key directly — otherwise the next release re-breaks the gate for all PRs the same way.